/****************************************************
	文件：ABBuilder.cs
	作者：HuskyT
	邮箱：1005240602@qq.com
	日期：2020/9/4 14:7:10
	功能：AB包打包器
*****************************************************/
#define DONOT_DELETE_META_FILE //每次打包的时候，不需要删除 meta 文件

using System.IO;
using System.Xml.Serialization;
using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
using System.Runtime.Serialization.Formatters.Binary;

namespace HTFramework.ResKit
{
    public class ABBuilder
    {
        /// <summary>
        /// 缓存  类型一（基于文件夹下所有单个文件进行逐个打包）
        /// Key：ABName（Prefab名字）
        /// Value：依赖项路径，已经剔除冗余的（含后缀）
        /// </summary>
        private static Dictionary<string, List<string>> mPrefabDict = new Dictionary<string, List<string>>();
        /// <summary>
        /// 缓存  类型二（基于文件夹进行整体打包）
        /// Key：ABName
        /// Value：文件夹路径
        /// </summary>
        private static Dictionary<string, string> mFilesFolderDict = new Dictionary<string, string>();
        /// <summary>
        /// 当前已经缓存的路径列表（用于保证AB包无冗余）
        /// 缓存 当前已“打包”的文件路径：先缓存类型二文件夹路径；然后尝试缓存类型一文件路径、及其依赖项路径
        /// </summary>
        private static List<string> mCurrentAlreadyCachePathList = new List<string>();
        /// <summary>
        /// 缓存需要动态加载的AB包全路径，
        /// 用于优化AB配置表，不需要动态加载的就不用写入表内。
        /// 需要动态加载的：类型一 与 类型二 文件；
        /// 不需要动态加载的：类型一 文件的依赖项文件（不包括类型二在内的）
        /// </summary>
        private static List<string> mDynamicLoadABPathList = new List<string>();

        [MenuItem(FConsts.AB_BUILD_MENU_PATH, false, 1)]
        public static void Build()
        {
            System.TimeSpan beginTime = System.DateTime.Now.TimeOfDay;
            //1.清空缓存
            CacheClear();
            //2.准备文件夹
            PrepareDirectory();
            //3.加载AB配置文件
            ABConfig cfg = LoadAsset<ABConfig>(FConsts.PATH_ABCONFIG);
            //4.缓存类型二：
            foreach (var fileFolderInfo in cfg.FilesFolderInfoList)
            {
                if (mFilesFolderDict.ContainsKey(fileFolderInfo.ABName))
                {
                    Debug.LogErrorFormat("存在重复的AB包名：{0}", fileFolderInfo.ABName);
                    return;
                }
                else
                {
                    mFilesFolderDict.Add(fileFolderInfo.ABName, fileFolderInfo.Path); //缓存  类型二
                    mCurrentAlreadyCachePathList.Add(fileFolderInfo.Path); //添加到  缓存
                    mDynamicLoadABPathList.Add(fileFolderInfo.Path); //类型二  需要动态加载
                }
            }
            //5.缓存类型一：
            //获取指定文件夹下所有 Prefab 文件的 guid
            string[] guidArray = AssetDatabase.FindAssets("t:Prefab", cfg.PrefabsFolderPathList.ToArray());
            for (int i = 0; i < guidArray.Length; i++)
            {
                string prefabPath = AssetDatabase.GUIDToAssetPath(guidArray[i]); //将 guid 转换为文件路径（含后缀）
                GameObject prefabGo = LoadAsset<GameObject>(prefabPath);
                string abName = prefabGo.name;
                if (mPrefabDict.ContainsKey(abName))
                {
                    Debug.LogErrorFormat("存在重复的 Prefab 名：{0}", abName);
                    return;
                }
                EditorUtility.DisplayProgressBar("查找 Prefab 路径", string.Format("路径：{0}", prefabPath), i * 1.0f / guidArray.Length);
                mDynamicLoadABPathList.Add(prefabPath); //所有 类型一 的 Prefab 都需要动态加载
                if (ContainedPath(mCurrentAlreadyCachePathList, prefabPath) == false)
                {
                    string[] dependencies = AssetDatabase.GetDependencies(prefabPath); //当前 prefab 实际的依赖项文件路径，包含自身，路径含后缀
                    List<string> dependencyList = new List<string>(); //剔除冗余后的 依赖项文件路径
                    for (int j = 0; j < dependencies.Length; j++)
                    {
                        if (ContainedPath(mCurrentAlreadyCachePathList, dependencies[j]) == false &&
                            dependencies[j].EndsWith(".cs") == false)
                        {
                            dependencyList.Add(dependencies[j]);
                            mCurrentAlreadyCachePathList.Add(dependencies[j]); //添加到  缓存
                        }
                    }
                    mPrefabDict.Add(abName, dependencyList); //缓存  类型一
                }
            }
            //6.设置AB包名
            int index = 0;
            int count = mFilesFolderDict.Count + mPrefabDict.Count;
            foreach (var prefabPackage in mPrefabDict)
            {
                SetABName(prefabPackage.Key, prefabPackage.Value);
                EditorUtility.DisplayProgressBar("设置单个文件的AB包名", string.Format("ABName：{0}", prefabPackage.Key), index * 1.0f / count);
                index++;
            }
            foreach (var filesFolder in mFilesFolderDict)
            {
                SetABName(filesFolder.Key, filesFolder.Value);
                EditorUtility.DisplayProgressBar("设置文件夹的AB包名", string.Format("ABName：{0}", filesFolder.Key), index * 1.0f / count);
                index++;
            }
            //7.写入AB包配置表
            WriteABConfig();
            //8.打包
            BuildAssetBundle();
            //9.清除AB包名（防止meta文件变动导致Git需要提交）
            DeleteABNames();
            AssetDatabase.Refresh();
            //10.AB包配置表（Binary）打包
            BuildABCfg();
            AssetDatabase.Refresh();
            EditorUtility.ClearProgressBar();
            //11.显示总的时间消耗
            System.TimeSpan endTime = System.DateTime.Now.TimeOfDay;
            float sumTime = (endTime - beginTime).Seconds;
            float minutes = (float)System.Math.Floor(sumTime / 60.0f);
            float seconds = sumTime % 60.0f;
            string txt = minutes > 0 ? string.Format("共耗时：{0} 分 {1} 秒", minutes, seconds) : string.Format("共耗时：{0} 秒", seconds);
            Debug.LogFormat("打包完成！{0}", txt);
        }
        [MenuItem(FConsts.ABCONFIG_BUILD_MENU_PATH, false, 2)]
        public static void BuildABConfig()
        {
            System.TimeSpan beginTime = System.DateTime.Now.TimeOfDay;
            //1.清空缓存
            CacheClear();
            //2.准备文件夹
            PrepareDirectory();
            //3.加载AB配置文件
            ABConfig cfg = LoadAsset<ABConfig>(FConsts.PATH_ABCONFIG);
            //4.缓存类型二：
            foreach (var fileFolderInfo in cfg.FilesFolderInfoList)
            {
                if (mFilesFolderDict.ContainsKey(fileFolderInfo.ABName))
                {
                    Debug.LogErrorFormat("存在重复的AB包名：{0}", fileFolderInfo.ABName);
                    return;
                }
                else
                {
                    mFilesFolderDict.Add(fileFolderInfo.ABName, fileFolderInfo.Path); //缓存  类型二
                    mCurrentAlreadyCachePathList.Add(fileFolderInfo.Path); //添加到  缓存
                    mDynamicLoadABPathList.Add(fileFolderInfo.Path); //类型二  需要动态加载
                }
            }
            //5.缓存类型一：
            //获取指定文件夹下所有 Prefab 文件的 guid
            string[] guidArray = AssetDatabase.FindAssets("t:Prefab", cfg.PrefabsFolderPathList.ToArray());
            for (int i = 0; i < guidArray.Length; i++)
            {
                string prefabPath = AssetDatabase.GUIDToAssetPath(guidArray[i]); //将 guid 转换为文件路径（含后缀）
                GameObject prefabGo = LoadAsset<GameObject>(prefabPath);
                string abName = prefabGo.name;
                if (mPrefabDict.ContainsKey(abName))
                {
                    Debug.LogErrorFormat("存在重复的 Prefab 名：{0}", abName);
                    return;
                }
                EditorUtility.DisplayProgressBar("查找 Prefab 路径", string.Format("路径：{0}", prefabPath), i * 1.0f / guidArray.Length);
                mDynamicLoadABPathList.Add(prefabPath); //所有 类型一 的 Prefab 都需要动态加载
                if (ContainedPath(mCurrentAlreadyCachePathList, prefabPath) == false)
                {
                    string[] dependencies = AssetDatabase.GetDependencies(prefabPath); //当前 prefab 实际的依赖项文件路径，包含自身，路径含后缀
                    List<string> dependencyList = new List<string>(); //剔除冗余后的 依赖项文件路径
                    for (int j = 0; j < dependencies.Length; j++)
                    {
                        if (ContainedPath(mCurrentAlreadyCachePathList, dependencies[j]) == false &&
                            dependencies[j].EndsWith(".cs") == false)
                        {
                            dependencyList.Add(dependencies[j]);
                            mCurrentAlreadyCachePathList.Add(dependencies[j]); //添加到  缓存
                        }
                    }
                    mPrefabDict.Add(abName, dependencyList); //缓存  类型一
                }
            }
            //6.设置AB包名
            int index = 0;
            int count = mFilesFolderDict.Count + mPrefabDict.Count;
            foreach (var prefabPackage in mPrefabDict)
            {
                SetABName(prefabPackage.Key, prefabPackage.Value);
                EditorUtility.DisplayProgressBar("设置单个文件的AB包名", string.Format("ABName：{0}", prefabPackage.Key), index * 1.0f / count);
                index++;
            }
            foreach (var filesFolder in mFilesFolderDict)
            {
                SetABName(filesFolder.Key, filesFolder.Value);
                EditorUtility.DisplayProgressBar("设置文件夹的AB包名", string.Format("ABName：{0}", filesFolder.Key), index * 1.0f / count);
                index++;
            }
            //7.写入AB包配置表
            WriteABConfig();
            //8.打包
            //BuildAssetBundle();
            //9.清除AB包名（防止meta文件变动导致Git需要提交）
            DeleteABNames();
            AssetDatabase.Refresh();
            //10.AB包配置表（Binary）打包
            BuildABCfg();
            AssetDatabase.Refresh();
            EditorUtility.ClearProgressBar();
            //11.显示总的时间消耗
            System.TimeSpan endTime = System.DateTime.Now.TimeOfDay;
            float sumTime = (endTime - beginTime).Seconds;
            float minutes = (float)System.Math.Floor(sumTime / 60.0f);
            float seconds = sumTime % 60.0f;
            string txt = minutes > 0 ? string.Format("共耗时：{0} 分 {1} 秒", minutes, seconds) : string.Format("共耗时：{0} 秒", seconds);
            Debug.LogFormat("AB包配置表打包完成！{0}", txt);
        }


        #region PrivateFunc
        /// <summary>
        /// 清空缓存
        /// </summary>
        private static void CacheClear()
        {
            mPrefabDict.Clear();
            mFilesFolderDict.Clear();
            mCurrentAlreadyCachePathList.Clear();
            mDynamicLoadABPathList.Clear();
        }
        /// <summary>
        /// 准备打包需要用到的文件夹
        /// </summary>
        private static void PrepareDirectory()
        {
            //AB包
            CreateDirectory(FConsts.PATH_AB);
            //Xml
            int xmlIndex = FConsts.PATH_AB_CONFIG_XML.LastIndexOf("/");
            string xmlDirectoryPath = FConsts.PATH_AB_CONFIG_XML.Remove(xmlIndex, FConsts.PATH_AB_CONFIG_XML.Length - xmlIndex);
            CreateDirectory(xmlDirectoryPath);
            //Binary
            int binaryIndex = FConsts.PATH_AB_CONFIG_BINARY.LastIndexOf("/");
            string binaryDirectoryPath = FConsts.PATH_AB_CONFIG_BINARY.Remove(binaryIndex, FConsts.PATH_AB_CONFIG_BINARY.Length - binaryIndex);
            CreateDirectory(binaryDirectoryPath);
        }
        /// <summary>
        /// 设置类型二（基于文件夹进行打包的）的AB包名
        /// path：文件夹路径（Assets/xxx...）
        /// </summary>
        private static void SetABName(string abName, string path)
        {
            AssetImporter assetImporter = AssetImporter.GetAtPath(path);
            if (assetImporter == null)
                Debug.LogErrorFormat("SetABName Error！\nAB包路径出错，路径：{0}", path);
            else
                assetImporter.assetBundleName = abName;
        }
        /// <summary>
        /// 设置类型一（prefab及其依赖项）的AB包名
        /// 依赖项是经过类型二剔除后的
        /// </summary>
        private static void SetABName(string abName, List<string> dependenciesName)
        {
            for (int i = 0; i < dependenciesName.Count; i++)
            {
                SetABName(abName, dependenciesName[i]);
            }
        }
        /// <summary>
        /// 打包
        /// </summary>
        private static void BuildAssetBundle()
        {
            //删除多余的AB包：
            string[] allABNames = AssetDatabase.GetAllAssetBundleNames(); //当前所有AB包包名
            DirectoryInfo directoryInfo = new DirectoryInfo(FConsts.PATH_AB);
            FileInfo[] fileInfos = directoryInfo.GetFiles("*", SearchOption.AllDirectories); //获取AB文件夹下所有文件信息
            for (int i = 0; i < fileInfos.Length; i++)
            {
                if (ContainedName(allABNames, fileInfos[i].Name)) continue; //AB包名未变动
#if DONOT_DELETE_META_FILE
                if (fileInfos[i].Name.EndsWith(".meta")) continue; //不删除 meta 文件
#endif
                if (File.Exists(fileInfos[i].FullName)) File.Delete(fileInfos[i].FullName); //删除
                //部分文件不需要打印变动提示信息
                if (fileInfos[i].Name.EndsWith(".manifest") ||
                    fileInfos[i].Name == "StreamingAssets" ||
                    fileInfos[i].Name == FConsts.CONFIG_ABNAME) continue;
                Debug.LogWarningFormat("AB包 {0} 已经删除或改名", fileInfos[i].Name);
            }
            //打包：
            //使用LZ4压缩，压缩率不如LZMA；可以加载指定资源，无需一次解压全部资源
            BuildPipeline.BuildAssetBundles(FConsts.PATH_AB, BuildAssetBundleOptions.ChunkBasedCompression, EditorUserBuildSettings.activeBuildTarget);
        }
        /// <summary>
        /// 写入AB配置
        /// </summary>
        private static void WriteABConfig()
        {
            //获取 AB包 数据：
            //Key：文件全路径；Value：文件所在的AB包的包名（所有的AB包信息）
            Dictionary<string, string> abInfoDict = new Dictionary<string, string>();
            string[] allABNames = AssetDatabase.GetAllAssetBundleNames(); //当前所有AB包包名
            for (int i = 0; i < allABNames.Length; i++)
            {
                //AB包内的文件的路径列表
                string[] paths = AssetDatabase.GetAssetPathsFromAssetBundle(allABNames[i]);
                foreach (var path in paths)
                {
                    if (path.EndsWith(".cs")) continue;
                    //Debug.LogFormat("{0}（AB包）内资源全路径：\n{1}", allABNames[i], path);
                    if (ContainedPath(mDynamicLoadABPathList, path))
                        abInfoDict.Add(path, allABNames[i]); //待写入  AB包配置表
                }
            }
            //写入数据：
            AssetBundleConfig cfg = new AssetBundleConfig(); //AB包配置表
            cfg.ABDataList = new List<ABData>();
            int index = 0;
            foreach (var abInfo in abInfoDict)
            {
                EditorUtility.DisplayProgressBar("写入AB包配置表", string.Format("AssetPath：{0}", abInfo.Key), index * 1.0f / abInfoDict.Count);
                ABData data = new ABData();
                data.Path = abInfo.Key;
                data.Crc = CRC32.GetCrc32(abInfo.Key);
                data.ABName = abInfo.Value;
                data.AssetName = abInfo.Key.Remove(0, abInfo.Key.LastIndexOf("/") + 1);
                data.ABDependencyList = new List<string>();
                string[] dependencies = AssetDatabase.GetDependencies(abInfo.Key); //当前文件所有的依赖项（全路径）
                foreach (var dependency in dependencies)
                {
                    if (dependency.EndsWith(".cs")) continue; //依赖项不能是 脚本
                    if (dependency == abInfo.Key) continue; //依赖项不能是 自身
                    string abName = "";
                    if (abInfoDict.TryGetValue(dependency, out abName) == false) continue;
                    if (abName == abInfo.Value) continue; //依赖项不能是 自身
                    if (data.ABDependencyList.Contains(abName) == false)
                        data.ABDependencyList.Add(abName);
                }
                cfg.ABDataList.Add(data);
            }
            //写入Xml
            ToXml(cfg, FConsts.PATH_AB_CONFIG_XML);
            //写入Binary（二进制不需要Path，只需要Crc）
            //foreach (var item in cfg.ABDataList)
            //{
            //    item.Path = "";
            //}
            ToBinary(cfg, FConsts.PATH_AB_CONFIG_BINARY);
        }
        /// <summary>
        /// 清除AB包名（防止meta文件变动导致Git需要提交）
        /// </summary>
        private static void DeleteABNames()
        {
            string[] oldABNames = AssetDatabase.GetAllAssetBundleNames();
            for (int i = 0; i < oldABNames.Length; i++)
            {
                AssetDatabase.RemoveAssetBundleName(oldABNames[i], true);
                EditorUtility.DisplayProgressBar("清除AB包名", string.Format("AB包名：{0}", oldABNames[i]), i * 1.0f / oldABNames.Length);
            }
        }
        /// <summary>
        /// AB配置表打包
        /// </summary>
        private static void BuildABCfg()
        {
            //设置AB包名
            string path = "Assets" + FConsts.PATH_AB_CONFIG_BINARY.Replace(Application.dataPath, string.Empty);
            SetABName(FConsts.CONFIG_ABNAME, path);
            //打包
            //使用LZ4压缩，压缩率不如LZMA；可以加载指定资源，无需一次解压全部资源
            BuildPipeline.BuildAssetBundles(FConsts.PATH_AB, BuildAssetBundleOptions.ChunkBasedCompression, EditorUserBuildSettings.activeBuildTarget);
            //清除AB包名（防止meta文件变动导致Git需要提交）
            DeleteABNames();
            Debug.LogFormat("{0} 打包完成.", FConsts.CONFIG_ABNAME);
        }
        #endregion


        #region HelperFunc
        /// <summary>
        /// 指定容器 array 中是否已经包含指定路径 path
        /// </summary>
        private static bool ContainedPath(IEnumerable<string> array, string path)
        {
            foreach (var item in array)
            {
                //path.Replace(mFliterList[i], string.Empty)[0] == '/'是用于避免：
                //Assets/GameData 和 Assets/GameDataxxx/atk.prefab
                //第二个包含第一个，但第二个和第一个不属于同一个文件夹
                if (path == item ||
                    path.Contains(item) && path.Replace(item, string.Empty)[0] == '/' ||
                    item.Contains(path) && item.Replace(path, string.Empty)[0] == '/')
                    return true;
            }
            return false;
        }
        /// <summary>
        /// 指定容器 array 中是否已经包含指定文件名
        /// </summary>
        private static bool ContainedName(IEnumerable<string> array, string assetName)
        {
            foreach (var item in array)
            {
                if (assetName == item)
                    return true;
            }
            return false;
        }
        /// <summary>
        /// 新建文件夹
        /// </summary>
        private static void CreateDirectory(string folderFullPath)
        {
            if (Directory.Exists(folderFullPath) == false)
            {
                Directory.CreateDirectory(folderFullPath);
                Debug.LogFormat("创建文件夹，文件夹路径：\n{0}", folderFullPath);
            }
        }
        /// <summary>
        /// 编辑器下的资源加载方式（路径：相对于Assets下的路径（Assets/xxx/xxx...））
        /// </summary>
        private static T LoadAsset<T>(string assetPath) where T : Object
        {
            return AssetDatabase.LoadAssetAtPath<T>(assetPath);
        }
        /// <summary>
        /// 将对象序列化为Xml文件
        /// </summary>
        private static void ToXml<T>(T obj, string fullPath)
        {
            if (File.Exists(fullPath)) File.Delete(fullPath); //存在则删除
            //Step1：文件流——创建xml文件
            FileStream fs = new FileStream(fullPath, FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite);
            //Step2：写入流
            StreamWriter sw = new StreamWriter(fs, System.Text.Encoding.UTF8);
            //Step3：创建xml序列化器
            XmlSerializer xmlSerializer = new XmlSerializer(obj.GetType());
            //Step4：序列化对象到写入流中
            xmlSerializer.Serialize(sw, obj);
            //Step5：关闭流
            sw.Close();
            fs.Close();
        }
        /// <summary>
        /// 将对象序列化为二进制文件
        /// </summary>
        private static void ToBinary<T>(T obj, string fullPath)
        {
            if (File.Exists(fullPath)) File.Delete(fullPath); //存在则删除
            //Step1：文件流——创建二进制文件
            FileStream fs = new FileStream(fullPath, FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite);
            //Step2：创建二进制序列化器
            BinaryFormatter binaryFormatter = new BinaryFormatter();
            //Step3：序列化对象到文件流中
            binaryFormatter.Serialize(fs, obj);
            //Step4：关闭流
            fs.Close();
        }
        #endregion
    }
}
